"""
Helper methods

Contains commonly used helper functions
"""

import wx
import sys
import os
import socket

from time import sleep
from traceback import format_exc
from urllib import quote, unquote
from urlparse import urlsplit, urlunsplit

from wx.lib import masked

from BitTornado.bencode import bdecode
from BitTornado.zurllib import urlopen

# List of common encodings
encodings = ["utf-8",  # Try UTF8 first (should work for ASCII data too)
             "ascii",  # Try ascii next (if utf-8 failed, this may fail too)
             sys.getfilesystemencoding(), # Default system encoding...
             "mbcs",   # Try mbcs next -- may work for most things under windows?
             "utf-16",
             "iso-8859-1",
             "iso-8859-2",
             "iso-8859-3",
             "iso-8859-4",
             "iso-8859-5",
             "iso-8859-6",
             "iso-8859-7",
             "iso-8859-8",
             "iso-8859-9",
             "iso-8859-10",
             "iso-8859-13",
             "iso-8859-14",
             "iso-8859-15",
             "chinese",
             "big5",
             "big5hkscs",
             "korean",
             "shiftjis",
             "eucjp"]

################################################################

def existsAndIsReadable(filename):
    """
    Check to see if a file both exists and is readable
    """
    return os.access(filename, os.F_OK) and os.access(filename, os.R_OK)

def intersection(list1, list2):
    """
    Intersection of two lists (or dictionaries)
    """
    
    # One (or both) of the lists is None
    if list1 is None:
        if list2 is not None and isinstance(list2, dict):
            # list2 is a dict
            return {}
        # list2 is either None or a list
        return []
    elif list2 is None:
        if isinstance(list1, dict):
            # list1 is a dict
            return {}
        # list1 is a list
        return []
    
    # (Order matters slightly so that has_key is called fewer times)
    if len(list1) < len(list2):
        smaller = list1
        bigger = list2
    else:
        smaller = list2
        bigger = list1
    
    int_dict = {}
    if isinstance(bigger, dict):
        bigger_dict = bigger
    else:
        bigger_dict = {}
        for e in bigger:
            bigger_dict[e] = 1
    for e in smaller:
        if e in bigger_dict:
            int_dict[e] = bigger_dict[e]
    return int_dict.keys()

def union(list1, list2):
    """
    Union of two dictionaries
    """
    if list1 is None:
        list1 = {}
    if list2 is None:
        list2 = {}
    
    # (Order matters slightly so that has_key is called fewer times)
    if len(list1) < len(list2):
        smaller = list1
        bigger = list2
    else:
        smaller = list2
        bigger = list1    
    
    if isinstance(bigger, dict):
        union_dict = bigger
    else:
        union_dict = {}
        for e in bigger:
            union_dict[e] = bigger[e]
    for e in smaller:
        union_dict[e] = smaller[e]
    return union_dict

def difference(list1, list2):
    """
    Difference of two dictionaries
    (A - B)
    """
    if list2 is None:
        return list1
    if list1 is None:
        return {}
        
    diff_dict = list1.copy()
    for e in list2:
        if e in diff_dict:
            del diff_dict[e]
    return diff_dict

def _getClientSocket(host, port):
    """
    Get a socket to send on
    """
    s = None
    errormsg = None
    
    for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM):
        af, socktype, proto, canonname, sa = res
        try:
            s = socket.socket(af, socktype, proto)
        except socket.error, msg:
            s = None
            
            errormsg = str(msg)
            continue

        try:
            s.connect(sa)
        except socket.error, msg:
            s.close()
            s = None
            
            errormsg = str(msg)
            continue
        break
        
    return (s, errormsg)
    
def _getServerSocket(host, port):
    """
    Get a socket to listen on
    """
    s = None
    errormsg = None

    for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
        af, socktype, proto, canonname, sa = res
        try:
            s = socket.socket(af, socktype, proto)
        except socket.error, msg:
            s = None
            
            errormsg = str(msg)

            continue
        try:
            s.bind(sa)
            s.listen(1)
        except socket.error, msg:
            s.close()
            s = None
            
            errormsg = str(msg)
            continue
        break

    return (s, errormsg)

def getSocket(host, port, sockettype = "client", attempt = 5):
    """
    Get a socket (either client or server)
    Will make up to 5 attempts to get the socket
    """
    s = None
    errormsg = None
    
    tries = 0

    while s is None and tries < attempt:
        try:
            if sockettype == "server":
                s, errormsg = _getServerSocket(host, port)
            else:
                s, errormsg = _getClientSocket(host, port)
        except:
            s = None
            print format_exc()
                
        if s is None:           
            # Try several times, increase in time each try
            sleep(0.01 * tries)
            tries += 1
            
    if s is None:
        print
        print "didn't get socket?"
        print "host:    " + str(host)
        print "port:    " + str(port)
        print "error:   " + str(errormsg)
        print

                       
    return s

   
#
# Multiple methods for getting free diskspace
#
try:
    # Unix
    from os import statvfs
    def getfreespace(path):
        if isinstance(path, unicode):
            try:
                path = path.encode(sys.getfilesystemencoding())
            except:
                # Unicode issues
                return False
            
        s = os.statvfs(path)
        size = s.f_bavail * long(s.f_bsize)
        return size
except:
    if (sys.platform[:3] == 'win'):
        try:
            # Windows if win32all extensions are installed
            import win32file
            try:
                # Win95 OSR2 and up
                test = win32file.GetDiskFreeSpaceEx(".")
                def getfreespace(path):
                    return win32file.GetDiskFreeSpaceEx(path)[0]
            except:
                # Original Win95
                # (2GB limit on partition size, so this should be
                #  accurate except for mapped network drives)
                def getfreespace(path):
                    [spc, bps, nfc, tnc] = win32file.GetDiskFreeSpace(path)
                    return long(nfc) * long(spc) * long(bps)
        except ImportError:
            # Windows if win32all extensions aren't installed
            # (parse the output from the dir command)
            def getfreespace(path):
                sizestring = "?"
                
                try:
                    # Try getting the size of either the drive
                    # or network path for the path that was fed in
                    try:
                        drive, tail = os.path.splitdrive(path)
                    except:
                        drive = None
                    try:
                        unc, rest = os.path.splitunc(path)
                    except:
                        unc = None
                        
                    if drive:
                        # Looks like a drive
                        path = drive
                    elif unc:
                        # Looks like a network path
                        path = unc
            
                    # Get the output from calling "dir" on the path
                    # (use /a to get all files, just in case the directory is empty)
                    # (use /w so that we have fewer lines to process)
                    mystdin, mystdout = os.popen2("dir /a /w " + "\"" + path + "\"")
                    
                    # Get the last non-empty line
                    lastline = ""
                    for line in mystdout:
                        line = line.strip()
                        if line:
                            lastline = line
            
                    parts = lastline.split(" ")
                        
                    # Look at the fourth part (should be the size)
                    if len(parts) >= 4:
                        part = parts[4]
                        part = part.replace(",", "")
                        sizestring = part
                except:
                    pass
                
                try:
                    # See if we were able to get the size information
                    if sizestring == "?":
                        # If in doubt, just return something really large
                        # (1 yottabyte)
                        size = 2**80L
                    else:
                        size = long(sizestring)
                except:
                    size = 2**80L
                
                return size
    else:
        # Any other cases
        # TODO: support for Mac? (will statvfs work with OS X?)
        def getfreespace(path):
            # If in doubt, just return something really large
            # (1 yottabyte)
            return 2**80L
            
            
def stopTorrentsIfNeeded(torrentlist):
    # Error : all selected torrents must be inactive to get extracted
    showDialog = True

    # See which torrents are active
    activetorrents = [torrent for torrent in torrentlist if torrent.status.isActive()]

    # Ask to stop other torrents if necessary
    if activetorrents > 0:
        singleTorrent = len(activetorrents) == 1
        for torrent in activetorrents:
            if torrent.dialogs.stopIfNeeded(showDialog, singleTorrent):
                # Torrent was stopped, don't show the dialog anymore
                showDialog = False
            else:
                # Selected not to stop the torrent, return False
                return False
    
    # At this point all selected torrents should be stopped
    return True

def forceunicode(text, firstencoding = None):
    """
    Convert a text string to unicode
    """
    # text is already unicode, don't need to do anything
    if isinstance(text, unicode):
        return text
    
    if firstencoding and firstencoding not in encodings:
        encodingstouse = encodings.copy()
        encodingstouse.insert(0, firstencoding)
    else:
        encodingstouse = encodings
    
    for encoding in encodingstouse:
        try:
            newstring = unicode(text, encoding)
#            print "worked using: " + encoding
            return newstring
        except:
            pass
    
    # Failed... (allow the fallback method of backslashreplace)
    return unicode(text, errors="backslashreplace")

def to_utf8(obj):
    if isinstance(obj, unicode):
        return obj.encode('utf8')
    elif isinstance(obj, list) or isinstance(obj, tuple):
        return [to_utf8(i) for i in obj]
    elif isinstance(obj, dict):
        d = {}
        for k in obj:
            d[to_utf8(k)] = to_utf8(obj[k])
        return d
    else:
        return obj

def comma_format(s):
    """
    Add commas every third position
    """
    r = str(s)
    for i in range(len(r)-3, 0, -3):
        r = r[:i]+','+r[i:]
    return(r)

def get_metainfo(src, openoptions = 'rb', style = "file", cookies = None):
    """
    Get the metainfo for a torrent
    """
    if src is None:
        return None
    
    metainfo = None
    try:
        metainfo_file = None
        
        if style == "rawdata":
            # Raw bencoded data
            return bdecode(src)
        
        if style == "url":
            # We're getting a url
            
            url_splitted = urlsplit(src)
            try:
                url_to_open = urlunsplit([url_splitted[0], url_splitted[1], quote(unquote(url_splitted[2])), url_splitted[3], url_splitted[4]])
            except:
                url_to_open = src

            metainfo_file = urlopen(url_to_open, encoding = None, cookies = cookies)     
            
        elif os.access(src, os.R_OK):
            # We're getting a file that exists
            metainfo_file = open(src, openoptions)
        
        if metainfo_file is not None:
            metainfo = bdecode(metainfo_file.read(), sloppy = 1)
            metainfo_file.close()
    except:
        if metainfo_file is not None:
            try:
                metainfo_file.close()
            except:
                pass
        metainfo = None
    return metainfo

def removeEmptyDir(basedir, removesubdirs = True):
    """
    Remove an empty directory
    
    if removesubdirs, try removing any empty subdirectories
    """
    # remove subdirectories
    if removesubdirs:
        for root, dirs, files in os.walk(basedir, topdown = False):
            for name in dirs:
                dirname = os.path.join(root, name)

                # Only try to delete if it exists
                if os.access(dirname, os.F_OK):
                    if not os.listdir(dirname):
                        os.rmdir(dirname)
    #remove folder
    if os.access(basedir, os.F_OK):
        if not os.listdir(basedir):
            os.rmdir(basedir)

def isPathRelative(path):
    if len(path) < 2 or path[1] != ':' and path[:2] != '\\\\':
        return True
    return False

def makeNumCtrl(parent, value, integerWidth = 6, fractionWidth = 0, min = 0, max = None, size = wx.DefaultSize):
    if size != wx.DefaultSize:
        autoSize = False
    else:
        autoSize = True
    return masked.NumCtrl(parent, 
                          value = value, 
                          size = size, 
                          integerWidth = integerWidth, 
                          fractionWidth = fractionWidth, 
                          allowNegative = False, 
                          min = min, 
                          max = max, 
                          groupDigits = False, 
                          useFixedWidthFont = False,
                          autoSize = autoSize)
    
def getInfoFromFont(font):
    """
    Get a dictionary with information about a font
    """
    try:
        if font.Ok():
            font_to_use = font
        else:
            # Use the default system font
            font_to_use = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
            
        fontinfo = {'name': font_to_use.GetFaceName(), 
                    'size': font_to_use.GetPointSize(), 
                    'style': font_to_use.GetStyle(), 
                    'weight': font_to_use.GetWeight() }
    except:
        fontinfo = {'name': "", 
                    'size': 8, 
                    'style': wx.FONTSTYLE_NORMAL, 
                    'weight': wx.FONTWEIGHT_NORMAL }

    return fontinfo
        
def getFontFromInfo(fontinfo):
    size = fontinfo['size']
    name = fontinfo['name']
    style = fontinfo['style']        
    weight = fontinfo['weight']
            
    try:
        font = wx.Font(size, wx.DEFAULT, style, weight, faceName = name)
    except:
        font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
    
    return font

_invalid_filename_char = ''
for i in range(32):
    _invalid_filename_char += chr(i)
_invalid_filename_char += '"*/:<>?\\|'

# Try to get the maxium length of a file
try:
    # Use Unix method
    import os
    _max_filename_length = os.statvfs(".").f_namemax
except:
    if sys.platform == 'win32':
        # Use Windows method
        _max_filename_length = 250
    else:    
        # Don't use a real limit
        _max_filename_length = 2**80L

def fixInvalidName(name):
    """
    Check ensure that a name is a valid filename
    """
    # Make sure the name is in unicode first
    name = forceunicode(name)
    
    if not name or name.startswith("."):
        # Make sure the name isn't empty and doesn't start with . or ..
        name = "_" + name[1:]
    
    if len(name) > _max_filename_length:
        # Windows size limit for a filename
        name = name[:_max_filename_length]
        
    for c in _invalid_filename_char:
        # Replace invalid characters with underscores
        name = name.replace(c, '_')
    
    return name

def fixWindowsName(name, unit = False):
    """
    Check if str is a valid Windows file name
    (or unit name if unit is true)
    
    If the filename isn't valid: returns a fixed name
    If the filename is valid: returns an empty string
    """
    if unit and (len(name) != 2 or name[1] != ':'):
        return 'c:'
    if not name or name == '.' or name == '..':
        return '_'
    if unit:
        name = name[0]
    fixed = False
    if len(name) > 250:
        name = name[:250]
        fixed = True
    fixedname = ''
    spaces = 0
    for c in name:
        if c in _invalid_filename_char:
            fixedname += '_'
            fixed = True
        else:
            fixedname += c
            if c == ' ':
                spaces += 1
    if fixed:
        return fixedname
    elif spaces == len(name):
        # contains only spaces
        return '_'
    else:
        return ''

def findUniqueFileName(dest, src = ''):
    """
    If file dest exists, returns a new name with '_' and 3 digits added
    If it can't be renamed this way, asks user for a new name
    returns None if a new name could not be set
    if src is 'web' or 'scan', user is not asked if automatic renaming fails
    """
    if not os.path.exists(dest):
        return dest
    
    destlen = len(dest)
    if destlen < 247:
        # Renaming will add '_' + 3 digit to the original name
        i = 1
        # To be sure the dot is inside the file name (and not in the rest of the path)
        if os.path.split(dest)[1].find('.') == -1:
            dot = destlen
        else:
            dot = dest.rfind('.')
        while True:
            newdest = dest[:dot] + ('_%03u' % i) + dest[dot:]
            if not os.path.exists(newdest):
                break
            i += 1
            if i == 1000:
                break
        if i != 1000:
            return newdest
    if src in ["web", "command", "scan"]:
        return None
    
    while True:
        # Impossible to find an automatic new name ; ask user for one
        dl = wx.FileDialog(None, _('Choose unique filename'), '', '', '*.*', wx.SAVE)
        result = dl.ShowModal()
        if result != wx.ID_OK:
            dl.Destroy()
            return None
        newdest = dl.GetPath()
        dl.Destroy()
        if not os.path.exists(newdest):
            return newdest
        
def getIcon(fileExtension):
    if wx.TheMimeTypesManager is None:
        return None, None, None
    FileInfo =  wx.TheMimeTypesManager.GetFileTypeFromExtension(fileExtension)
    if FileInfo is None:
        return None, None, None
    IconInfo = FileInfo.GetIconInfo()
    if IconInfo is None:
        return None, None, None
    return IconInfo
    
################################################################

class LimitedNumCtrl(masked.NumCtrl):
    """
    Extends masked.NumCtrl
    
    Overrides some of the base class's behaviors involving limiting values
    """
    def __init__(self, parent, value = 0, integerWidth = 6, fractionWidth = 0, min = 0, max = None):
        if max is not None:            
            # Control doesn't need to be any bigger than
            # the maximum value that will fit in it
            maxwidth = len(str(max))
            if maxwidth < integerWidth:
                integerWidth = maxwidth
        
        masked.NumCtrl.__init__(self, parent)
        
        self.SetValue(value)
        self.SetIntegerWidth(integerWidth)
        self.SetFractionWidth(fractionWidth)
        self.SetAllowNegative(False)
        self.SetBounds(min, max)
        self.SetGroupDigits(False)
        self.SetAutoSize(True)
        self.SetUseFixedWidthFont(False)
        
    def GetValue(self):
        """
        Return the limited version of the value
        """
        value = masked.NumCtrl.GetValue(self)
        realvalue = value
        
        min = self.GetMin()
        max = self.GetMax()
        if min is not None and value < min:
            realvalue = min
        if max is not None and value > max:
            realvalue = max

        if value != realvalue:
            self.SetValue(value)

        return realvalue
        
    def SetValue(self, value):
        """
        Set the value, then make sure it's within the proper bounds
        """
        min = self.GetMin()
        max = self.GetMax()
        if min is not None and value < min:
            value = min
        if max is not None and value > max:
            value = max
                        
        masked.NumCtrl.SetValue(self, value)
